cc-pocket · direction a · approval correction

Approval Protocol Handoff v1

The approval sheet redrawn against the real V1 protocol. Consequence summary, the Effects row and the Recommended block are gone — the daemon does not send them, so the client cannot show them. The sheet now leads with the request title and tool, renders the literal payload, and treats permission duration as a decision rather than as a stated fact. Sessions and Chat are untouched; this file supersedes only the approval portions of A Master Core v1 and A Master Proofs v1.

5 frames
402 × 874 · 1:1
iPhone 17
portrait · minimum
no new tokens
direction a locked
Contract · SecureApprovalSheet
Anatomy of a non-dismissible sheet

All five frames use this presentation, not the generic PocketSheet. Nothing about it is decorative: every part of the contract exists to stop a security decision resolving without the user.

scrim · taps swallowed
pinned header
state · queue
timer or waiting
scroll body
title · tool
risk
payload
project
pinned decisions
grabber
swipe‑down
scrim tap
system back
explicit decision
daemon timeout
withdrawal
Three zones, always

Header and decision bar are pinned; only the body scrolls. A long command, an expanded diff or 200% type grows the body alone, so the actions cannot leave the screen.

No silent resolution

System back keeps the sheet open and returns nothing to the daemon. Scrim taps are consumed. There is no grabber and the drag gesture is disabled, so the sheet cannot be flung away mid-decision.

Exits

An explicit decision resolves the request and closes the sheet. AskWithdrawn(TIMED_OUT) turns it into TimeoutTerminal, which the user may then dismiss. An ordinary withdrawal or a session close removes the sheet immediately and advances the queue. System back, scrim taps and swipe resolve nothing at any point, and the client never resolves a request on its own clock.

Terminal state · TimeoutTerminal

Only the timed-out withdrawal produces it. Decisions are replaced by the daemon's outcome and a single Dismiss, and this is the one state in which the sheet may be dismissed. Other withdrawals leave nothing behind to dismiss.

Removed in this pass
consequenceSummary heading
Effects row
Recommended block
Permission · This task only

The first three are not in the V1 payload. The fourth was a fact about a decision the user had not made yet; scope now lives in the action labels.

01 · M2 ordinary · dark · 402×874
9:41
Refactor auth module
Claude · default permission
cc-pocket · feat/auth-refactor
The parser now emits exactly one event when a frame is split across chunks.
Approval required 1 of 3
18s automatically denied when time runs out
Upload coverage to Codecov
Run command Bash
Medium risk external upload
assessed 9:41:02 · net.egress.upload
./gradlew test && bash scripts/upload-coverage.sh
Project ~/proj/app/cc-pocket
Deny
Allow once
Retry safer
Allow for task Bash(./gradlew test:*)
More options session scope available
Corrects
Everything on this screen has a source: title, tool, rule and inputPreview from the ask; workdir from session context; queue from repository state; risk, reason and assessedAt from the async update. The invented consequence sentence and the Effects row are gone.

Permission is no longer stated as a fact. The four labels are the scopes — Allow once and Allow for task differ only in how long the grant lives, and the rule under the task action names exactly what would be remembered.

Four equal tiles, one accent. Task scope carries the accent only because grantOptions offered it; the geometry does not change when it is absent.

Session grant exists but is not on the main card. It sits behind More options, one deliberate step away, so the broadest grant is never the fastest tap.
02 · M2 danger · dark · 402×874
9:41
Refactor auth module
cc-pocket · feat/auth-refactor
Approval required 2 of 3
24s automatically denied when time runs out
Reset the working tree
Run command Bash
High risk deletes files
Removes the build directory and every untracked file in the working tree, including anything not yet committed.
assessed 9:41:26 · fs.delete.recursive
rm -rf build/ && git clean -fdx
Project ~/proj/app/cc-pocket
Deny
Allow once
Retry safer
Allow for task Bash(git clean:*)
Corrects
No “Recommended: Deny” block, because the daemon supplies no recommendation. Policy is expressed by emphasis instead: least privilege takes the accent, so Allow once is the easiest grant to reach and the widest grant is the hardest.

This emphasis is triggered by PermissionAsk.danger alone. A high risk value arriving from PermissionRiskUpdated changes how the assessment presents — it never moves the accent or alters the action set, because an async signal must not silently rewrite the decision the user is looking at.

Allow for task is demoted, not removed. It keeps the same tile, the same height, the same hit area and the same boundary — only its ink drops to secondary. The user can still choose it; the layout just stops arguing for it.

Danger reads before the command. Risk level, the supplied reason and dangerNote sit in one bounded block above the payload, so the sentence that explains the deletion cannot be scrolled past.

Deny keeps a danger boundary rather than a fill. Two filled controls side by side would make the pair read as a choice between two equally endorsed paths.
03 · legacy pre-M2 · light · 402×874
9:41
Fix flaky socket test
cc-pocket · fix/socket-test
Approval required
30s automatically denied when time runs out
Run the protocol test suite
Run command Bash
./gradlew :protocol:test --rerun-tasks
Project ~/proj/app/cc-pocket
Legacy permission model. This peer sends no grant options, no risk assessment and no queue position, and no timeout — the client applies its 30 second fallback.
Deny
Allow once
Always allow Bash(./gradlew:*)
Corrects
The earlier legacy frame claimed the request waits until the user decides. It does not: with timeoutSec absent the client falls back to 30 seconds and auto-denies, so the timer and the fail-closed sentence are shown.

grantOptions is null, so the M2 grid does not apply. The legacy triple renders instead, and Always allow — the only persistent grant this protocol has — carries the rule it would write.

Allow once still takes the accent. A peer that cannot describe risk is not a reason to make the broadest grant the easiest one.

The compatibility note states what is missing and what the client did about it. It is neutral body text, never a badge, and it makes no claim about whether the command is safe.
04 · one-off · review shell · dark · 402×874
9:41
Prepare 1.6 release
cc-pocket · main
Approval required
45s automatically denied when time runs out
Rotate deploy keys on build-01
Run command Bash
Medium risk remote host
ssh deploy@build-01 \
  'cd /srv/cc-pocket && \
   ./scripts/rotate-keys.sh --confirm'
Confirmed individually
This command is never remembered. Every run is confirmed on its own and the decision is written to the session record.
Project ~/proj/app/cc-pocket
Deny
Allow once
Proves
neverRemember removes three actions, not just their labels. No task grant, no session grant, no Always allow and no Retry safer — the pair that remains is exactly what the request permits.

Two decisions keep the same tile height and the same 13pt radius as the four-up grid, so a one-off request does not read as a different component.

The review note is a real property of the request, so it sits in the body as a hairline band rather than beside the buttons where it could be mistaken for advice.

The body still scrolls under a pinned header and a pinned decision bar — shown here mid-scroll with the multi-line command and the record note both in view.
05 · noAutoDeny · dark · 402×874
9:41
Prepare 1.6 release
cc-pocket · main
Approval required
Waiting for you
No response countdown. The daemon keeps this request active until you decide or it withdraws it.
Rewrite the release script
Edit file Edit
Medium risk modifies release tooling
assessed 9:41:44 · repo.tooling.write
scripts/release.sh
@@ -18,7 +18,9 @@
  set -euo pipefail
− VERSION=$(cat VERSION)
+ VERSION=$(git describe --tags)
+ git tag -s "v$VERSION" -m "release"
  ./gradlew publish
Project ~/proj/app/cc-pocket
Deny
Allow once
Retry safer
Proves
noAutoDeny is a header modifier, not a variant. It removes the countdown, the bar and the fail-closed sentence, and states what is true instead: there is no response countdown and the daemon keeps the request active. Renewal is bounded by the daemon's own hard floor, so the client shows no timer and promises no infinite lifetime — ∞ is the established waiting-state symbol, not a guarantee.

The risk block stays exactly as it is in the timed frames. No timer must never read as no danger, so the assessment keeps its position, its weight and its colour.

The decision set still comes from the family, not from the modifier. grantOptions is non-null and offers only once, so DecisionV2 builds Deny, Allow once and Retry safer. Retry safer takes the full width rather than leaving an empty cell, and no disabled tile is drawn for a scope the daemon did not offer.

No queue counter, because the repository reported no queue. The header keeps its state line and drops the number rather than falling back to 1 of 1.
A

Selection pipeline

Four stages, composed. The variants are not mutually exclusive rows.
A · route
Is this a sheet at all?
Questions leave the pipeline here.
B · terminal event
Has it already ended?
Only a timed-out withdrawal leaves something on screen.
C · decision family
Which action set?
Exactly one family. This is the only exclusive choice in the pipeline.
D · modifiers
What else is true?
Independent. Any number apply at once to the family chosen in C.
Stage
Condition
Result
Notes
A · routequestions
questions is non-empty.
In-chat QuestionCard. No SecureApprovalSheet is created.
Outside this handoff. An ask that carries questions is a question, not a permission decision, and never reaches stages B–D.
B · terminal eventTIMED_OUT
AskWithdrawn(TIMED_OUT)
TimeoutTerminal. Read-only outcome, decisions replaced by Dismiss.
The outcome is the daemon's, not the client's. This is the only state the user may dismiss.
WITHDRAWN · session close
Ordinary withdrawal, or the session ends.
Sheet is removed immediately and the queue advances.
Nothing dismissible is left behind. Not every withdrawal becomes a terminal state — only TIMED_OUT does.
C · decision familyDecisionOneOff
neverRemember === true or the handoff review shell.
Deny · Allow once
Takes precedence over grantOptions even when scopes are offered. The review shell adds the record note in the body. Frame 04.
DecisionV2
grantOptions != null
Deny · Allow once · Retry safer · one tile per offered grant.
Built from the flags, not from a fixed set. Session scope moves under More options. Frames 01, 02, 05.
DecisionLegacy
grantOptions == null
Deny · Allow once · Always allow
Accent on Allow once. 30s fallback timer. Compatibility note in the body. Frame 03.
D · modifiersnoAutoDeny
noAutoDeny === true
Header only: the countdown is replaced by the waiting state.
Not a decision variant. The action set still comes from stage C, so it combines with ordinary, danger and one-off alike. Frame 05.
danger
PermissionAsk.danger === true — and only this flag.
Emphasis only: accent moves to Allow once, broader grants demote, Deny takes a danger boundary.
The action set is unchanged. A high async risk value does not trigger this — it changes only how RiskAssessment presents. Frame 02.
queue · risk
Repository queue state, and PermissionRiskUpdated, when each really exists.
Header counter; RiskAssessment block in the body.
Independent of each other and of every other stage. Risk can arrive after the sheet opens and never rewrites the decisions.

Stage C is the only exclusive choice; stage D composes on top of whatever C produced. Availability is derived only from capability flags — an action the daemon did not offer is not rendered, never rendered disabled, and no cell is reserved for it. Two offered grants fill a 2×2 grid; one offered grant produces a 2 + 1 grid at the same tile size.

B

Field sources

Every string on the sheet, and where it comes from.
Displayed field
Source
Renders when
Missing behaviour
Title
PermissionAsk.title
Always. It is the first line of the body.
Kind and tool line is promoted to the heading. The client never writes a title from the command.
Kind · tool
PermissionAsk.tool
Always. Kind is a fixed client label per tool; the tool name is verbatim.
Not possible — tool is required.
Payload
PermissionAsk.inputPreview
PermissionAsk.diff
Verbatim, in mono, wrapping on characters. diff renders as a diff; otherwise the literal preview.
Block omitted. No paraphrase, no truncation with an ellipsis, no “see desktop”.
Risk · reason
PermissionRiskUpdated.risk
.reason · .reasonCodes · .assessedAt
Badge on risk. Reason only when the event carries one. Codes and assessedAt render as one quiet mono line.
No badge at all. Absence is never drawn as low risk, and never as a neutral or green mark.
Danger note
PermissionAsk.danger
PermissionAsk.dangerNote
danger true. The note renders inside the risk block, above the payload.
Block omitted; danger without a note still applies the danger emphasis to the decisions.
Rule sublabel
PermissionAsk.rule
On persistent grants only — Allow for task, Always allow. It is what would be written.
Action keeps its label with no sublabel. Tile height is unchanged.
Project
session.workdir
Session context supplies a working directory.
Row omitted. Never substituted with the repository name or a guessed path.
Queue n of m
repository queue state
Position and total are both known and the total is greater than one.
Counter dropped, state label kept. No “1 of 1”.
Timer
PermissionAsk.timeoutSec
PermissionAsk.noAutoDeny
noAutoDeny false. Number, bar and the fail-closed sentence render together.
timeoutSec missing on a legacy peer falls back to 30s and still auto-denies. noAutoDeny true shows the waiting state instead.
Questions
PermissionAsk.questions
Never on this sheet. When non-empty the ask routes to the in-chat QuestionCard.
SecureApprovalSheet is not created. Questions are answered in Chat, not approved here.
consequenceSummary
not in V1
Never. Do not render.
The sheet leads with title and tool. The client does not summarise a command in prose.
effects
not in V1
Never. Do not render.
The literal payload is the effects list. No derived bullets.
recommendation · reason
not in V1
Never. Do not render.
Policy is carried by which action holds the accent. No sentence claims a recommended choice.
C

Component mapping

SecureApprovalSheet

Owns the presentation contract: no grabber, no drag, swallowed scrim, intercepted back, three pinned-scroll-pinned zones and safe-area insets. Never the generic PocketSheet.

ApprovalHeader

State mark and label, optional queue counter, and either the countdown with its fail-closed sentence or the waiting state. Pinned.

ApprovalPayload

One of command, diff or plan, chosen by which field arrived. Mono, verbatim, character wrapping. No summarising layer above it.

RiskAssessment

Badge, optional reason, optional codes and assessedAt. Subscribes to PermissionRiskUpdated and can appear after the sheet opens; it never reserves space before it does.

ProjectRow

Hairline row carrying workdir from session context. Omitted when the session has none.

DecisionV2

Grid built from grantOptions plus Deny and Retry safer. Owns emphasis: widest offered grant on ordinary, Allow once when danger is set. Hosts the More options row when session scope exists.

Implementation delta

The current client gates V2 on grantOptions containing task. This handoff changes the gate to grantOptions != null, so a grant-aware peer offering only once no longer falls back to legacy and no longer offers an Always allow the peer never authorised. Rendered actions must match the offered flags exactly.

DecisionLegacy

Deny, Allow once, Always allow. Same tile metrics as DecisionV2 so the two never read as different components.

DecisionOneOff

Deny and Allow once only. Selected by neverRemember or the review shell, and it ignores any offered grants.

TimeoutTerminal

Replaces the decision bar only after AskWithdrawn(TIMED_OUT). Read-only body, single Dismiss, and the only state in which the sheet may be closed without a decision. An ordinary withdrawal or a session close removes the sheet outright and produces no TimeoutTerminal.

D

Acceptance checklist

Every frame is correct at 402×874 with no clipping and no horizontal scroll.
Taller devices expand the scroll body only; header and decision bar keep their heights.
Safe areas respected top and bottom; the decision bar clears the home indicator.
No keyboard can appear over the sheet; the sheet has no text input.
No consequence sentence, no effects list, no recommendation — in any variant, any locale.
No permission-duration fact is shown before a decision is made.
Legacy fixture with no timeoutSec shows 30s and auto-denies at zero.
Back, swipe-down and scrim tap resolve nothing; only a decision, an authoritative timeout or a withdrawal ends the sheet.
Action set matches capability flags exactly: nothing offered is hidden, nothing unoffered is drawn.
Body text ≥ 4.5:1 dark and light. Every decision tile carries the shared Direction A boundary — 28% ink dark, 32% light — and demotion never weakens it.
All decision tiles share one height and one hit area, including demoted and sublabelled tiles.
Dynamic Type is tested as its own pass; 360dp is not a release gate for V1.
Scope of this handoff

Approval only. The Sessions and Chat direction in A Master Core v1 and A Master Proofs v1 stays locked, including the approval entry points on those screens. Where those files disagree with this one about the approval sheet, this file wins.